﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Xml;

using FirstFloor.Documents.IO;
using FirstFloor.Documents.Pdf.ColorSpaces;
using FirstFloor.Documents.Pdf.Fonts;
using System.Globalization;

namespace FirstFloor.Documents.Pdf
{
    /// <summary>
    /// Parses page content streams and returns Silverlight compatible XAML.
    /// </summary>
    internal class PageParser
    {
        private static Dictionary<string, OperatorHandler> handlers = CreateOperatorHandlers();

        private delegate void OperatorHandler(PageParserContext context, params object[] args);

        private static Dictionary<string, OperatorHandler> CreateOperatorHandlers()
        {
            var handlers = new Dictionary<string, OperatorHandler>();

            handlers["b"] = (context, args) => {    // Close, fill, and stroke path using nonzero winding number rule
                handlers["h"](context, args);
                handlers["B"](context, args);
            };
            handlers["B"] = (context, args) => {    // Fill and stroke path using nonzero winding number rule
                RenderPath(context, FillRule.Nonzero, true, true);
            };
            handlers["b*"] = (context, args) => {   // Close, fill, and stroke path using even-odd rule
                handlers["h"](context, args);
                handlers["B*"](context, args);
            };
            handlers["B*"] = (context, args) => {   // Fill and stroke path using even-odd rule
                RenderPath(context, FillRule.EvenOdd, true, true);
            };
            handlers["BDC"] = (context, args) => {  // Begin a marked-content sequence with an associated property list
                //var tag = (string)args[0];

                // TODO: implement
                //throw new NotImplementedException("BDC");
            };
            handlers["BI"] = (context, args) => {   // Begin inline image object
                // TODO: implement
                //throw new NotImplementedException("BI");
            };
            handlers["BMC"] = (context, args) => {  // Begin a marked-content sequence
                var tag = (string)args[0];
                // TODO: implement
                //throw new NotImplementedException("BMC");
            };
            handlers["BT"] = (context, args) => {   // Begin text object
                context.State.Tm = context.State.Tlm = Matrix.Identity;
            };
            handlers["BX"] = (context, args) => {   // Begin compatibility section
                // TODO: implement
                //throw new NotImplementedException("BX");
            };
            handlers["c"] = (context, args) => {    // Append curved segment to path
                var x1 = (double)args[0];
                var y1 = (double)args[1];
                var x2 = (double)args[2];
                var y2 = (double)args[3];
                var x3 = (double)args[4];
                var y3 = (double)args[5];

                context.AddPathCurveSegment(x1, y1, x2, y2, x3, y3);
            };
            handlers["cm"] = (context, args) => {   // Concatenate matrix to current transformation matrix
                context.State.Ctm = context.State.Ctm.Concat((double)args[0], (double)args[1], (double)args[2], (double)args[3], (double)args[4], (double)args[5]);
            };
            handlers["cs"] = (context, args) => {   // Set color space for nonstroking operations
                var colorSpace = ColorSpace.CreateColorSpace(context, (string)args[0]);

                context.State.NonstrokingColorSpace = colorSpace;
                context.State.NonstrokingColor = colorSpace.DefaultColor;
            };
            handlers["CS"] = (context, args) => {   // Set color space for stroking operations
                var colorSpace = ColorSpace.CreateColorSpace(context, (string)args[0]);

                context.State.StrokingColorSpace = colorSpace;
                context.State.StrokingColor = colorSpace.DefaultColor;
            };
            handlers["d"] = (context, args) => {    // Set line dash pattern
                var pattern = new StringBuilder();
                foreach (var value in ((ArrayObject)args[0]).ToArray<double>()) {
                    if (pattern.Length > 0) {
                        pattern.Append(" ");
                    }
                    pattern.Append(value.Format());
                }
                context.State.LineDashPattern = pattern.ToString();
                context.State.LineDashOffset = (double)args[1];
            };
            handlers["d0"] = (context, args) => {   // Set glyph width in Type 3 font
                // TODO: implement
                //throw new NotImplementedException("d0");
            };
            handlers["d1"] = (context, args) => {   // Set glyph width and bounding box in Type 3 font
                // TODO: implement
                //throw new NotImplementedException("d1");
            };
            handlers["Do"] = (context, args) => {   // Invoke named XObject
                var name = (string)args[0];
                var xobject = context.Resources.Get<DictionaryObject>("XObject");
                var dictionary = xobject.Get<DictionaryObject>(name);
                var subType = dictionary.Get<string>("Subtype");
                if (subType == "Form") {
                    RenderForm(context, dictionary);
                }
                else if (subType == "Image") {
                    RenderImage(context, name, dictionary);
                }
                else {
                    // ignore
                }
            };
            handlers["DP"] = (context, args) => {   // Define marked-content point with property list
                // TODO: implement
                //throw new NotImplementedException("DP");
            };
            handlers["EI"] = (context, args) => {   // End inline image object
                // TODO: implement
                //throw new NotImplementedException("EI");
            };
            handlers["EMC"] = (context, args) => {  // End a marked-content sequence
                // TODO: implement
                //throw new NotImplementedException("EMC");
            };
            handlers["ET"] = (context, args) => {   // End text object
                // nothing here
            };
            handlers["EX"] = (context, args) => {   // End compatibility section
                // nothing here
            };
            handlers["f"] = (context, args) => {    // Fill the path, using the nonzero winding number rule
                RenderPath(context, FillRule.Nonzero, true, false);
            };
            handlers["F"] = handlers["f"];          // Equal to f
            handlers["f*"] = (context, args) => {   // Fill the path, using the even-odd rule
                RenderPath(context, FillRule.EvenOdd, true, false);
            };
            handlers["g"] = (context, args) => {    // Set gray level for nonstroking operations
                var g = (double)args[0];

                context.State.NonstrokingColorSpace = DeviceGray.Default;
                context.State.NonstrokingColor = new double[] { g };
            };
            handlers["G"] = (context, args) => {    // Set gray level for stroking operations
                var g = (double)args[0];

                context.State.StrokingColorSpace = DeviceGray.Default;
                context.State.StrokingColor = new double[] { g };
            };
            handlers["gs"] = (context, args) => {   // Set parameters from graphics state parameter dictionary
                var name = (string)args[0];
                var extgstate = context.Resources.Get<DictionaryObject>("ExtGState");
                var state = extgstate.Get<DictionaryObject>(name);

                foreach (var key in state.AllKeys) {
                    if (key == "LW") {          // The line width
                        handlers["w"](context, state.Get<object>(key));
                    }
                    else if (key == "LC") {     // The line cap style
                        handlers["J"](context, state.Get<object>(key));
                    }
                    else if (key == "LJ") {     // The line join style
                        handlers["j"](context, state.Get<object>(key));
                    }
                    else if (key == "ML") {     // The miter limit
                        handlers["M"](context, state.Get<object>(key));
                    }
                    else if (key == "D") {      // The line dash pattern
                        var value = state.Get<ArrayObject>(key);
                        handlers["d"](context, value[0], value[1]);
                    }
                    else if (key == "Font") {   // Font settings
                        var value = state.Get<ArrayObject>(key);

                        // PDF is a seriously messed up specification, set font with gs operator uses dictionary object
                        // instead of setting state Tf property
                        context.UpdateCurrentFont(value.Get<DictionaryObject>(0));
                        context.State.Tfs = value.Get<double>(1);
                    }
                    else {
                        // not supported
                    }
                }

            };
            handlers["h"] = (context, args) => {    // Close the current subpath
                context.AddPathData("Z");
            };
            handlers["i"] = (context, args) => {    // Set flatness tolerance
                // TODO: implement
                //throw new NotImplementedException("i");
            };
            handlers["ID"] = (context, args) => {   // Begin inline image data
                // TODO: implement
                //throw new NotImplementedException("ID");
            };
            handlers["j"] = (context, args) => {    // Set line join style
                var style = (int)(double)args[0];
                if (style == 0) {
                    context.State.LineJoin = PenLineJoin.Miter;
                }
                else if (style == 1) {
                    context.State.LineJoin = PenLineJoin.Round;
                }
                else if (style == 2) {
                    context.State.LineJoin = PenLineJoin.Bevel;
                }
            };
            handlers["J"] = (context, args) => {    // Set line cap style
                var style = (int)(double)args[0];
                if (style == 0) {
                    context.State.LineCap = PenLineCap.Flat;
                }
                else if (style == 1) {
                    context.State.LineCap = PenLineCap.Round;
                }
                else if (style == 2) {
                    context.State.LineCap = PenLineCap.Square;
                }
            };
            handlers["k"] = (context, args) => {    // Set CMYK color for nonstroking operations
                var c = (double)args[0];
                var m = (double)args[1];
                var y = (double)args[2];
                var k = (double)args[3];

                context.State.NonstrokingColorSpace = DeviceCMYK.Default;
                context.State.NonstrokingColor = new double[] { c, m, y, k };
            };
            handlers["K"] = (context, args) => {    // Set CMYK color for stroking operations
                var c = (double)args[0];
                var m = (double)args[1];
                var y = (double)args[2];
                var k = (double)args[3];

                context.State.StrokingColorSpace = DeviceCMYK.Default;
                context.State.StrokingColor = new double[] { c, m, y, k };
            };
            handlers["l"] = (context, args) => {     // Append a straight line segment to path
                var x = (double)args[0];
                var y = (double)args[1];
                context.AddPathData("L{0},{1}", x.Format(), y.Format());
                context.CurrentPathPoint = new Point(x, y);
            };
            handlers["m"] = (context, args) => {    // Begin a new subpath by moving the current point
                var x = (double)args[0];
                var y = (double)args[1];
                context.AddPathData("M{0},{1}", x.Format(), y.Format());
                context.CurrentPathPoint = new Point(x, y);
            };
            handlers["M"] = (context, args) => {    // Set miter limit
                context.State.MiterLimit = (double)args[0];
            };
            handlers["MP"] = (context, args) => {   // Define marked-content point
                // TODO: implement
                //throw new NotImplementedException("MP");
            };
            handlers["n"] = (context, args) => {    // End path without filling or stroking
                context.ClearPathData();
            };
            handlers["q"] = (context, args) => {    // Save graphics state
                context.SaveState();
            };
            handlers["Q"] = (context, args) => {    // Restore graphics state
                context.RestoreState();
            };
            handlers["re"] = (context, args) => {   // Append a rectangle to the current path
                var x = (double)args[0];
                var y = (double)args[1];
                var width = (double)args[2];
                var height = (double)args[3];

                context.AddPathData("M{0},{1} L{2},{3} L{4},{5} L{6},{7} Z",
                    x.Format(), y.Format(),
                    (x + width).Format(), y.Format(),
                    (x + width).Format(), (y + height).Format(),
                    x.Format(), (y + height).Format());
            };
            handlers["rg"] = (context, args) => {   // Set RGB color for nonstroking operations
                var r = (double)args[0];
                var g = (double)args[1];
                var b = (double)args[2];

                context.State.NonstrokingColorSpace = DeviceRGB.Default;
                context.State.NonstrokingColor = new double[] { r, g, b };
            };
            handlers["RG"] = (context, args) => {   // Set RGB color for stroking operations
                var r = (double)args[0];
                var g = (double)args[1];
                var b = (double)args[2];

                context.State.StrokingColorSpace = DeviceRGB.Default;
                context.State.StrokingColor = new double[] { r, g, b };
            };
            handlers["ri"] = (context, args) => {   // Set color rendering intent
                // TODO: implement
                //throw new NotImplementedException("ri");
            };
            handlers["s"] = (context, args) => {    // Close and stroke the path.
                handlers["h"](context, args);
                handlers["S"](context, args);
            };
            handlers["S"] = (context, args) => {    // Stroke the path.
                RenderPath(context, FillRule.EvenOdd, false, true);
            };
            handlers["sc"] = (context, args) => {   // Set color for nonstroking operations
                context.State.NonstrokingColor = args.ToArray<double>();
            };
            handlers["SC"] = (context, args) => {   // Set color for stroking operations
                context.State.StrokingColor = args.ToArray<double>();
            };
            handlers["scn"] = (context, args) => {       // Set color for nonstroking operations (ICCBased and special color spaces)
                var colorSpace = context.State.NonstrokingColorSpace;
                if (colorSpace.Name == "Pattern") {
                    // TODO: implement
                }
                else {
                    handlers["sc"](context, args);
                }
            };
            handlers["SCN"] = (context, args) => {       // Set color for stroking operations (ICCBased and special color spaces)
                var colorSpace = context.State.NonstrokingColorSpace;
                if (colorSpace.Name == "Pattern") {
                    // TODO: implement
                }
                else {
                    handlers["SC"](context, args);
                }
            };
            handlers["sh"] = (context, args) => {   // Paint area defined by shading pattern
                // TODO: implement
                //throw new NotImplementedException("sh");
            };
            handlers["T*"] = (context, args) => {   // Move to start of next text line
                handlers["Td"](context, 0D, context.State.Tl);
            };
            handlers["Tc"] = (context, args) => {   // Set character spacing
                context.State.Tc = (double)args[0];
            };
            handlers["Td"] = (context, args) => {   // Move text position
                var textToUserSpace = context.State.Tfs * context.State.Tm.M22;

                context.State.Tm = context.State.Tlm = context.State.Tlm.Concat(1, 0, 0, 1, (double)args[0] * textToUserSpace, (double)args[1] * textToUserSpace);
            };
            handlers["TD"] = (context, args) => {   // Move text position and set leading
                handlers["Td"](context, args);
                handlers["TL"](context, (double)args[1]);
            };
            handlers["Tf"] = (context, args) => {   // Set text font and size
                if (args.Length != 2) {
                    return;
                }
                context.State.Tf = (string)args[0];
                context.State.Tfs = (double)args[1];

                context.UpdateCurrentFont();

            };
            handlers["Tj"] = (context, args) => {   // Show text
                var font = context.CurrentFont;
                if (font != null) {
                    RenderGlyphs(context, font.ParseGlyphContent(args));
                }
            };
            handlers["TJ"] = (context, args) => {   // Show text, allowing individual glyph positioning
                if (args.Length == 0) {
                    return;
                }
                var parts = (ArrayObject)args[0];
                var font = context.CurrentFont;
                if (font != null) {
                    RenderGlyphs(context, font.ParseGlyphContent(parts.ToArray<object>()));
                }
            };
            handlers["TL"] = (context, args) => {   // Set text leading
                context.State.Tl = (double)args[0];
            };
            handlers["Tm"] = (context, args) => {   // Set text matrix and text line matrix
                context.State.Tm = context.State.Tlm = new Matrix(
                    (double)args[0], (double)args[1],
                    (double)args[2], (double)args[3],
                    (double)args[4], (double)args[5]);
            };
            handlers["Tr"] = (context, args) => {   // Set text rendering mode
                context.State.Tmode = (int)(double)args[0];
            };
            handlers["Ts"] = (context, args) => {   // Set text rise
                context.State.Trise = (double)args[0];
            };
            handlers["Tw"] = (context, args) => {   // Set word spacing
                context.State.Tw = (double)args[0];
            };
            handlers["Tz"] = (context, args) => {   // Set horizontal text scaling
                context.State.Th = (double)args[0];
            };
            handlers["v"] = (context, args) => {    // Append curved segment to path
                var x1 = context.CurrentPathPoint.X;
                var y1 = context.CurrentPathPoint.Y;
                var x2 = (double)args[0];
                var y2 = (double)args[1];
                var x3 = (double)args[2];
                var y3 = (double)args[3];

                context.AddPathCurveSegment(x1, y1, x2, y2, x3, y3);
            };
            handlers["w"] = (context, args) => {    // Set the line width in the graphics state
                context.State.LineWidth = (double)args[0];
            };
            handlers["W"] = (context, args) => {    // Set clipping path using nonzero winding number rule
                // TODO: implement
                //throw new NotImplementedException("W");
            };
            handlers["W*"] = (context, args) => {    // Set clipping path using nonzero winding number rule
                // TODO: implement
                //throw new NotImplementedException("W*");
            };
            handlers["y"] = (context, args) => {    // Append curved segment to path
                var x1 = (double)args[0];
                var y1 = (double)args[1];
                var x2 = context.CurrentPathPoint.X;
                var y2 = context.CurrentPathPoint.Y;
                var x3 = (double)args[2];
                var y3 = (double)args[3];

                context.AddPathCurveSegment(x1, y1, x2, y2, x3, y3);
            };
            handlers["'"] = (context, args) => {    // Move to next line and show text
                handlers["T*"](context);
                handlers["Tj"](context, args[0]);
            };
            handlers["\""] = (context, args) => {   // Set word and character spacing, move to next line, and show text
                handlers["Tw"](context, args[0]);
                handlers["Tc"](context, args[1]);
                handlers["'"](context, args[2]);
            };

            return handlers;
        }

        private static void RenderGlyphs(PageParserContext context, GlyphsContent content)
        {
            if (context.CurrentFont == null) {
                Debug.WriteLine("Skipping text, no current font");
                // ignore text that cannot be rendered
                return;
            }
            if (content == null) {
                Debug.WriteLine("Skipping text for a {0} font", context.CurrentFont.FontType);
                // ignore text that cannot be rendered
                return;
            }

            var font = context.CurrentFont;

            context.Writer.WriteStartElement("Glyphs");

            var trm = new Matrix(context.State.Tfs * context.State.Th / 100, 0, 0, context.State.Tfs, 0, context.State.Trise);
            trm = trm.Concat(context.State.Tm).Concat(context.State.Ctm);
            trm.OffsetY = context.MediaBox.Height - trm.OffsetY - trm.M22;      // flip y-axis

            var w0 = 0D;
            var indices = new StringBuilder();
            var prevCharIsSpace = false;
            var unicodeString = new StringBuilder();
            foreach(var index in content.Indices) {
                if (indices.Length > 0) {
                    indices.Append(';');
                }

                if (index.GlyphIndex.HasValue) {
                    indices.Append(index.GlyphIndex.Value);
                }

                // add character spacing
                var w = index.Width + (1000 * context.State.Tc);

                if (prevCharIsSpace) {
                    // add word spacing
                    w += (1000 * context.State.Tw);
                }

                w0 += w;
                indices.Append(string.Format(",{0}", (w / 10).Format()));       // glyph advance is in 100ths of em

                prevCharIsSpace = index.IsSpaceCharacter;

                unicodeString.Append(index.UnicodeCodePoint);
            }

            var tx = (w0 / 1000D * trm.M22) * context.State.Th / 100D;

            context.State.Tm = context.State.Tm.Concat(1, 0, 0, 1, tx, 0);

            if (unicodeString.Length != content.Indices.Length) {
                // skip writing unicode string entirely
            }
            else {
                if (unicodeString.Length > 0) {
                    // ensure string is properly escaped if starts with { character
                    if (unicodeString[0] == '{') {
                        unicodeString.Insert(0, new char[] { '{', '}' });
                    }

                    // write numeric entity
                    for (var i = 0; i < unicodeString.Length; i++) {
                        var c = unicodeString[i];
                        if (!XmlConvert.IsXmlChar(c)) {
                        }
                    }

                    context.Writer.WriteAttributeString("UnicodeString", unicodeString.ToString());
                }
            }

            context.Writer.WriteAttributeString("Indices", indices.ToString());
            context.Writer.WriteAttributeString("Fill", context.State.NonstrokingRGBColor.Format());
            context.Writer.WriteAttributeString("FontUri", font.RequestUri);
            context.Writer.WriteAttributeString("FontRenderingEmSize", Math.Abs(trm.M11).Format());
            context.Writer.WriteAttributeString("OriginX", trm.OffsetX.Format());
            context.Writer.WriteAttributeString("OriginY", (trm.OffsetY + trm.M22).Format());
            if (context.State.Th != 100) {
                context.Writer.WriteAttributeString("RenderTransform", string.Format("{0},0,0,1,0,0", (context.State.Th / 100).Format()));
            }
            context.Writer.WriteEndElement();

            // DEBUG
            //context.Writer.WriteStartElement("Line");
            //context.Writer.WriteAttributeString("Stroke", "Blue");
            //context.Writer.WriteAttributeString("StrokeThickness", "1");
            //context.Writer.WriteAttributeString("X1", trm.OffsetX.Format());
            //context.Writer.WriteAttributeString("Y1", trm.OffsetY.Format());
            //context.Writer.WriteAttributeString("X2", trm.OffsetX.Format());
            //context.Writer.WriteAttributeString("Y2", (trm.OffsetY + trm.M22).Format());

            //context.Writer.WriteEndElement();
        }

        private static void RenderPath(PageParserContext context, FillRule fillRule, bool fill, bool stroke)
        {
            context.Writer.WriteStartElement("Path");

            var data = context.GetPathData();
            context.ClearPathData();        // clear path
            if (fillRule == FillRule.Nonzero) {
                data = "F1 " + data;
            }

            var transform = context.State.Ctm.Concat(1, 0, 0, -1, 0, context.MediaBox.Height);      // flip y-axis

            context.Writer.WriteAttributeString("Data", data);
            context.Writer.WriteAttributeString("RenderTransform", transform.Format());

            if (fill) {
                context.Writer.WriteAttributeString("Fill", context.State.NonstrokingRGBColor.Format());
            }
            if (stroke) {
                context.Writer.WriteAttributeString("Stroke", context.State.StrokingRGBColor.Format());
                if (!string.IsNullOrEmpty(context.State.LineDashPattern)) {
                    context.Writer.WriteAttributeString("StrokeDashArray", context.State.LineDashPattern);
                }
                context.Writer.WriteAttributeString("StrokeDashCap", context.State.LineCap.Format());
                context.Writer.WriteAttributeString("StrokeDashOffset", context.State.LineDashOffset.Format());
                context.Writer.WriteAttributeString("StrokeEndLineCap", context.State.LineCap.Format());
                context.Writer.WriteAttributeString("StrokeLineJoin", context.State.LineJoin.Format());
                context.Writer.WriteAttributeString("StrokeMiterLimit", context.State.MiterLimit.Format());
                context.Writer.WriteAttributeString("StrokeStartLineCap", context.State.LineCap.Format());
                context.Writer.WriteAttributeString("StrokeThickness", context.State.LineWidth.Format());
            }

            context.Writer.WriteEndElement();
        }

        private static void RenderImage(PageParserContext context, string name, DictionaryObject dictionary)
        {
            var transform = context.State.Ctm;
            transform.OffsetY = context.MediaBox.Height - transform.OffsetY;      // flip y-axis


            var fileExtension = GetImageExtension(context, dictionary);

            context.Writer.WriteStartElement("Path");
            context.Writer.WriteAttributeString("Data", "M0,-1 L1,-1 L1,0 L0,0 Z");
            context.Writer.WriteAttributeString("RenderTransform", transform.Format());

            context.Writer.WriteStartElement("Path.Fill");
            context.Writer.WriteStartElement("ImageBrush");
            if (fileExtension != null) {
                context.Writer.WriteAttributeString("ImageSource", RequestHelper.CreateImageRequestUri(dictionary.Identifier.Value, fileExtension));
            }
            context.Writer.WriteEndElement();
            context.Writer.WriteEndElement();

            context.Writer.WriteEndElement();
        }

        private static string GetImageExtension(PageParserContext context, DictionaryObject imageDictionary)
        {
            var stream = context.Parser.ParseStreamObject(imageDictionary);
            var filter = stream.Filters.FirstOrDefault();

            if (filter == "DCTDecode") {
                return ".png";
            }
            else if (filter == null || filter == "FlateDecode" || filter == "CCITTFaxDecode") {
                return ContentType.FileExtensionRgbBitmap;
            }
            return null;
        }


        private static void RenderForm(PageParserContext context, DictionaryObject dictionary)
        {
            context.SaveState();

            var matrixObj = dictionary.Get<ArrayObject>("Matrix", null);
            var matrix = Matrix.Identity;
            if (matrixObj != null) {
                matrix = matrixObj.CreateMatrix();
            }

            var transform = context.State.Ctm = context.State.Ctm.Concat(matrix);
            var formContext = context.CreateFormContext(dictionary);

            transform.OffsetY = context.MediaBox.Height - formContext.MediaBox.Height - transform.OffsetY;      // flip y-axis

            // start new canvas
            context.Writer.WriteStartElement("Canvas");
            context.Writer.WriteAttributeString("RenderTransform", transform.Format());
            // TODO: clip canvas with bbox bounds
            
            var stream = context.Parser.ParseStreamObject(dictionary).OpenFilteredStream();
            ProcessContentStream(formContext, stream);
            
            context.Writer.WriteEndElement();
            context.RestoreState();
        }

        private static void ProcessContentStream(PageParserContext context, Stream stream)
        {
            using (var lexer = new PdfLexer(stream)) {
                var operands = new List<object>();
                while (lexer.HasNextToken()) {
                    var token = lexer.Peek();
                    if (token.Type == TokenType.Keyword) {
                        // execute the appropiate handler
                        OperatorHandler handler;
                        if (handlers.TryGetValue(token.Value, out handler)) {
                            //Debug.WriteLine("> {0}", token.Value);

                            handler(context, operands.ToArray());
                        }
                        else {
                            throw new PdfParseException(Resources.UnknownToken, token.Value);
                        }

                        operands.Clear();
                        lexer.Read();
                    }
                    else {
                        // read operand
                        operands.Add(lexer.ReadObject(context.Parser));
                    }
                }
            }
        }

        /// <summary>
        /// Parses the specified page using given object reader.
        /// </summary>
        /// <param name="pageObject">The page object.</param>
        /// <param name="settings">The settings.</param>
        /// <returns>The XAML.</returns>
        public string Parse(DictionaryObject pageObject, LoadSettings settings)
        {
            var result = new StringBuilder();

            using (var writer = XmlWriter.Create(result)) {
                var context = PageParserContext.Create(pageObject, writer, settings);

                writer.WriteStartElement(Xmlns.PrefixDocumentToolkit, "FixedPage", Xmlns.DocumentToolkit);
                writer.WriteAttributeString("xmlns", Xmlns.XamlPresentation);
                writer.WriteAttributeString("xmlns", Xmlns.PrefixXaml, null, Xmlns.Xaml);
                writer.WriteAttributeString("xmlns", Xmlns.PrefixDocumentToolkit, null, Xmlns.DocumentToolkit);

                // scale to 96 DPI
                var dpiScale = 96D / 72D;

                var width = context.CropBox.Width * dpiScale;
                var height = context.CropBox.Height * dpiScale;

                writer.WriteAttributeString("Width", width.Format());
                writer.WriteAttributeString("Height", height.Format());

                // scale entire page contents to 96 DPI and make sure cropbox is applied (actual clipping is added in FixedPageProcessor)
                var offsetX = -context.CropBox.Left * dpiScale;
                var offsetY = -context.CropBox.Top * dpiScale;

                writer.WriteStartElement("Canvas");
                writer.WriteAttributeString("RenderTransform", string.Format("{0},0,0,{0},{1},{2}", dpiScale.Format(), offsetX.Format(), offsetY.Format()));

                foreach (var content in GetContents(pageObject)) {
                    var stream = content.OpenFilteredStream();
                    ProcessContentStream(context, stream);
                }
                writer.WriteEndElement();
                
                // write EXPERIMENTAL WATERMARK
                //writer.WriteStartElement("TextBlock");
                //writer.WriteAttributeString("FontFamily", "Courier New");
                //writer.WriteAttributeString("FontSize", "14");
                //writer.WriteAttributeString("Foreground", "Red");
                //writer.WriteAttributeString("Text", "*EXPERIMENTAL* Document Toolkit 2 PDF Engine");
                //writer.WriteAttributeString("Canvas.Left", "10");
                //writer.WriteAttributeString("Canvas.Top", "10");
                //writer.WriteEndElement();

                // write warnings
                if (context.HasWarnings) {
                    writer.WriteStartElement(Xmlns.PrefixDocumentToolkit, "FixedPage.Warnings", Xmlns.DocumentToolkit);
                    writer.WriteStartElement(Xmlns.PrefixDocumentToolkit, "WarningCollection", Xmlns.DocumentToolkit);
                    foreach (var warning in context.Warnings) {
                        writer.WriteStartElement(Xmlns.PrefixDocumentToolkit, "Warning", Xmlns.DocumentToolkit);
                        writer.WriteAttributeString("Number", warning.Number.ToString(CultureInfo.InvariantCulture));
                        writer.WriteAttributeString("Text", warning.Text);
                        writer.WriteAttributeString("Level", warning.Level.ToString());
                        writer.WriteAttributeString("Source", warning.Source);
                        writer.WriteEndElement();
                    }
                    writer.WriteEndElement();
                    writer.WriteEndElement();
                }

                writer.WriteEndElement();
            }

            return result.ToString();
        }

        private IEnumerable<StreamObject> GetContents(DictionaryObject pageObject)
        {
            var contents = pageObject["Contents"];
            var contentsArray = contents as ArrayObject;
            if (contentsArray != null) {
                for (int i = 0; i < contentsArray.Count; i++) {
                    var dictionary = contentsArray.Get<DictionaryObject>(i);

                    yield return pageObject.Owner.ParseStreamObject(dictionary);
                }
            }
            else {
                yield return pageObject.Owner.ParseStreamObject((DictionaryObject)contents);
            }
        }
    }
}
